/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.persistence.hibernate.news;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.parsers.exception.PlainTextExtractorException;
import com.blandware.atleap.common.parsers.html.HTMLPlainTextExtractor;
import com.blandware.atleap.common.util.ConvertUtil;
import com.blandware.atleap.common.util.PartialCollection;
import com.blandware.atleap.common.util.QueryInfo;
import com.blandware.atleap.model.core.*;
import com.blandware.atleap.model.news.NewsItem;
import com.blandware.atleap.persistence.exception.DeleteException;
import com.blandware.atleap.persistence.hibernate.core.PageDAOHibernate;
import com.blandware.atleap.persistence.news.NewsDAO;
import org.hibernate.Hibernate;
import org.hibernate.type.Type;

import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;

/**
 * <p>Hibernate implementation of NewsDAO</p>
 * <p><a href="NewsDAOHibernate.java.html"><i>View Source</i></a></p>
 *
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.28 $ $Date: 2008/06/23 13:24:55 $
 */
public class NewsDAOHibernate extends PageDAOHibernate implements NewsDAO {

	/**
	 * Creates new instance of NewsDAOHibernate
	 */
	public NewsDAOHibernate() {
	}

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#createNewsItem(com.blandware.atleap.model.news.NewsItem, java.util.Map)
	 */
	public Long createNewsItem(NewsItem newsItem, Map linkedObjects) {
        newsItem.setUsageCounter(new Integer(0));
        newsItem.setLastUpdatedDatetime(new Date());
        Long newsItemId = (Long) getHibernateTemplate().save(newsItem);

		// title
		ContentField fTitle = new ContentField();
		fTitle.setIdentifier("title");
		fTitle.setType(ContentField.LINE_TYPE);
		fTitle.setInternal(Boolean.TRUE);
		newsItem.addContentField(fTitle);
		getHibernateTemplate().save(fTitle);

		// annotation
		ContentField fAnnotation = new ContentField();
		fAnnotation.setIdentifier("annotation");
		fAnnotation.setType(ContentField.HTML_TYPE);
		fAnnotation.setInternal(Boolean.TRUE);
		newsItem.addContentField(fAnnotation);
		getHibernateTemplate().save(fAnnotation);

		// body
		ContentField fBody = new ContentField();
		fBody.setIdentifier("body");
		fBody.setType(ContentField.HTML_TYPE);
		fBody.setInternal(Boolean.TRUE);
		newsItem.addContentField(fBody);
		getHibernateTemplate().save(fBody);

        // Fill in content field values for title, annotation and body
        // fields for every content locale
		List contentLocales = executeFind("from ContentLocale l", true, "query.listContentLocales");
		HTMLPlainTextExtractor extractor = new HTMLPlainTextExtractor();
		for ( int i = 0; i < contentLocales.size(); i++ ) {
			ContentLocale contentLocale = (ContentLocale) contentLocales.get(i);
			String localeIdentifier = contentLocale.getIdentifier();

            // values for title, annotation and body in that locale
			String valueTitle = (String) newsItem.getTitle().get(localeIdentifier);
			String valueAnnotation = (String) newsItem.getAnnotation().get(localeIdentifier);
			String valueBody = (String) newsItem.getBody().get(localeIdentifier);

            // skip a locale that all values for it are empty
			if ( (valueTitle == null || valueTitle.trim().length() == 0) && (valueAnnotation == null || valueAnnotation.trim().length() == 0) && (valueBody == null || valueBody.trim().length() == 0) ) {
				continue;
			}

			//title
			ContentFieldValue fvTitle = new ContentFieldValue();
			fvTitle.setContentLocale(contentLocale);
			if ( valueTitle != null ) {
				fvTitle.setSimpleValue(valueTitle);
			} else {
				fvTitle.setSimpleValue("");
			}
			fvTitle.setLastUpdatedDatetime(new Date());
			fTitle.addContentFieldValue(fvTitle);
			getHibernateTemplate().save(fvTitle);

			//annotation
			ContentFieldValue fvAnnotation = new ContentFieldValue();
			fvAnnotation.setContentLocale(contentLocale);
			if ( valueAnnotation != null ) {
				byte[] annotationBytes = ConvertUtil.convertToByteArray(valueAnnotation);
				fvAnnotation.setValue(annotationBytes);
				// get linked objects
				try {
					Set refs = new HashSet(extractor.extractAllRefs(new ByteArrayInputStream(annotationBytes), Constants.DEFAULT_ENCODING));
					List linkedPages = new ArrayList();
					List linkedResources = new ArrayList();
					for ( Iterator j = refs.iterator(); j.hasNext(); ) {
						String ref = (String) j.next();
						BaseObject linkedObject = (BaseObject) linkedObjects.get(ref);
						if ( linkedObject != null ) {
							if ( linkedObject instanceof ContentResource ) {
								linkedResources.add(linkedObject);
							} else if ( linkedObject instanceof Page ) {
								linkedPages.add(linkedObject);
							}
						}
					}
					fvAnnotation.setLinkedResources(linkedResources);
					fvAnnotation.setLinkedPages(linkedPages);
				} catch ( PlainTextExtractorException e ) {
					// log exception
					if ( log.isErrorEnabled() ) {
						StringWriter sw = new StringWriter();
						e.printStackTrace(new PrintWriter(sw));
						log.error(sw.toString());
					}
				}
			} else {
				fvAnnotation.setValue(new byte[0]);
			}
			fvAnnotation.setLastUpdatedDatetime(new Date());
			fAnnotation.addContentFieldValue(fvAnnotation);
			getHibernateTemplate().save(fvAnnotation);

			//body
			ContentFieldValue fvBody = new ContentFieldValue();
			fvBody.setContentLocale(contentLocale);
			if ( valueBody != null ) {
				byte[] bodyBytes = ConvertUtil.convertToByteArray(valueBody);
				fvBody.setValue(bodyBytes);
				// get linked objects
				try {
					Set refs = new HashSet(extractor.extractAllRefs(new ByteArrayInputStream(bodyBytes), Constants.DEFAULT_ENCODING));
					List linkedPages = new ArrayList();
					List linkedResources = new ArrayList();
					for ( Iterator j = refs.iterator(); j.hasNext(); ) {
						String ref = (String) j.next();
						BaseObject linkedObject = (BaseObject) linkedObjects.get(ref);
						if ( linkedObject != null ) {
							if ( linkedObject instanceof ContentResource ) {
								linkedResources.add(linkedObject);
							} else if ( linkedObject instanceof Page ) {
								linkedPages.add(linkedObject);
							}
						}
					}
					fvBody.setLinkedResources(linkedResources);
					fvBody.setLinkedPages(linkedPages);
				} catch ( PlainTextExtractorException e ) {
					// log exception
					if ( log.isErrorEnabled() ) {
						StringWriter sw = new StringWriter();
						e.printStackTrace(new PrintWriter(sw));
						log.error(sw.toString());
					}
				}
			} else {
				fvBody.setValue(new byte[0]);
			}
			fvBody.setLastUpdatedDatetime(new Date());
			fBody.addContentFieldValue(fvBody);
			getHibernateTemplate().save(fvBody);
		}

		return newsItemId;
	}

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#retrieveNewsItem(Long)
	 */
	public NewsItem retrieveNewsItem(Long newsItemId) {
		String hql = new StringBuffer("select newsItem, title.contentLocale.identifier, title.simpleValue, annotation.value, body.value ")
		        .append("from NewsItem as newsItem left outer join ")
		        .append("newsItem.contentFields titleField left outer join titleField.contentFieldValues as title left outer join ")
		        .append("newsItem.contentFields annotationField left outer join annotationField.contentFieldValues as annotation ")
		        .append("left outer join newsItem.contentFields bodyField left outer join bodyField.contentFieldValues as body ")
		        .append("where ")
		        .append("((title.contentLocale = annotation.contentLocale and title.contentLocale = body.contentLocale) or (title is null and annotation is null and body is null)) ")
		        .append("and titleField.identifier = 'title' ")
		        .append("and annotationField.identifier = 'annotation' and bodyField.identifier = 'body' and newsItem.id = ?")
		        .toString();

        // Get a list of quintuples (news item, locale identifier, title value,
        // annitation value, body value)
		List list = executeFind(hql, new Object[]{newsItemId});
        // Assign those values to corresponding news items -- get list of items with
        // title, annotation and body correctly assigned
		List result = setLocalizableFields(list);
		if ( result == null || result.size() <= 0 ) {
			return null;
		} else {
			return (NewsItem) result.get(0);
		}
	}

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#retrieveNewsItemLite(Long)
	 */
	public NewsItem retrieveNewsItemLite(Long newsItemId) {
		return (NewsItem) getHibernateTemplate().get(NewsItem.class, newsItemId);
	}

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#updateNewsItem(com.blandware.atleap.model.news.NewsItem, java.util.Map)
	 */
	public void updateNewsItem(NewsItem newsItem, Map linkedObjects) {
		getHibernateTemplate().update(newsItem);

		HTMLPlainTextExtractor extractor = new HTMLPlainTextExtractor();

        // Update internal fields (title, annotation and body) of this page
		for ( int i = 0; i < newsItem.getContentFields().size(); i++ ) {
			ContentField contentField = (ContentField) newsItem.getContentFields().get(i);
			Map values;
			boolean blob = true;
			if ( "title".equalsIgnoreCase(contentField.getIdentifier()) ) {
				values = newsItem.getTitle();
				blob = false;
			} else if ( "annotation".equalsIgnoreCase(contentField.getIdentifier()) ) {
				values = newsItem.getAnnotation();
			} else if ( "body".equalsIgnoreCase(contentField.getIdentifier()) ) {
				values = newsItem.getBody();
			} else {
				continue;
			}
			for ( int j = 0; j < contentField.getContentFieldValues().size(); j++ ) {
				ContentFieldValue contentFieldValue = (ContentFieldValue) contentField.getContentFieldValues().get(j);
				String localeIdentifier = contentFieldValue.getContentLocale().getIdentifier();
				String value = (String) values.get(localeIdentifier);
				if ( value != null ) {
					if ( !blob ) {
						contentFieldValue.setSimpleValue(value);
					} else {
						byte[] valueBytes = ConvertUtil.convertToByteArray(value);
						contentFieldValue.setValue(valueBytes);
						if ( linkedObjects != null ) {
							// get linked objects
							try {
								Set refs = new HashSet(extractor.extractAllRefs(new ByteArrayInputStream(valueBytes), Constants.DEFAULT_ENCODING));
								List linkedPages = new ArrayList();
								List linkedResources = new ArrayList();
								for ( Iterator k = refs.iterator(); k.hasNext(); ) {
									String ref = (String) k.next();
									BaseObject linkedObject = (BaseObject) linkedObjects.get(ref);
									if ( linkedObject != null ) {
										if ( linkedObject instanceof ContentResource ) {
											linkedResources.add(linkedObject);
										} else if ( linkedObject instanceof Page ) {
											linkedPages.add(linkedObject);
										}
									}
								}
								contentFieldValue.setLinkedResources(linkedResources);
								contentFieldValue.setLinkedPages(linkedPages);
							} catch ( PlainTextExtractorException e ) {
								// log exception
								if ( log.isErrorEnabled() ) {
									StringWriter sw = new StringWriter();
									e.printStackTrace(new PrintWriter(sw));
									log.error(sw.toString());
								}
							}
						}
					}
				} else {
					if ( !blob ) {
						contentFieldValue.setSimpleValue("");
					} else {
						contentFieldValue.setValue(new byte[0]);
					}
				}
				contentFieldValue.setLastUpdatedDatetime(new Date());
				contentFieldValue.getContentField().updateContentFieldValue(contentFieldValue);
				getHibernateTemplate().update(contentFieldValue);
			}
		}
	}

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#deleteNewsItem(com.blandware.atleap.model.news.NewsItem)
	 */
	public void deleteNewsItem(NewsItem newsItem) throws DeleteException {
        removeAssociations(newsItem);

		getHibernateTemplate().delete(newsItem);

		// break relations between roles and deleted news item
		List roles = new ArrayList(newsItem.getRoles());
		for ( int i = 0; i < roles.size(); i++ ) {
			Role role = (Role) roles.get(i);
			newsItem.removeRole(role);
		}
	}

	// ~ Additional methods ================================================================

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#listNewsItems(com.blandware.atleap.common.util.QueryInfo)
	 */
	public PartialCollection listNewsItems(QueryInfo queryInfo) {
		String whereClause = "";
		String orderByClause = "";
		if ( queryInfo != null ) {
			whereClause = queryInfo.getWhereClause();
			orderByClause = queryInfo.getOrderByClause();
		}

		if ( whereClause == null || whereClause.length() == 0 ) {
			whereClause = "";
		}
		if ( orderByClause != null && orderByClause.length() != 0 ) {
			orderByClause = " order by " + orderByClause;
		} else {
			orderByClause = " order by item.publicationDate desc, title.simpleValue asc";
		}

		List list;
		Integer total = null;

		String localeIdentifier = null;
		if ( queryInfo != null ) {
			if ( queryInfo.getQueryParameters() != null ) {
				localeIdentifier = (String) queryInfo.getQueryParameters().get("localeIdentifier");
			}
		}

		boolean localeIdentifierPresent = localeIdentifier != null && localeIdentifier.length() > 0;

		String hqlPart;
		ArrayList args = new ArrayList();
		if ( localeIdentifierPresent ) {
			args.add(localeIdentifier);
			hqlPart = new StringBuffer("from NewsItem as item ")
			        .append("left outer join item.contentFields as titleField ")
			        .append("left outer join titleField.contentFieldValues as title ")
			        .append("left outer join item.roles as role ")
			        .append("where ")
			        .append("titleField.identifier = 'title' ")
			        .append("and title.contentLocale.identifier = ? ")
			        .toString();
			if ( whereClause.length() > 0 ) {
				hqlPart += "and " + whereClause;
			}
		} else {
			hqlPart = "from NewsItem item left outer join item.roles as role ";
			if ( whereClause.length() > 0 ) {
				hqlPart += "where " + whereClause;
			}
		}

		if ( queryInfo != null && queryInfo.getLimit() != null && queryInfo.getOffset() != null ) {
			// query count
			String hqlForTotal = "select count(distinct item.id) " + hqlPart;
			total = findUniqueIntegerResult(hqlForTotal, args.toArray());
			if ( total == null ) {
				total = new Integer(0);
			}
		}

        // If we don't have any info about the total number of results yet or
        // we know that there's something that will be found, then fetch data
		if ( total == null || total.intValue() > 0 ) {
			String hql;
			if ( localeIdentifierPresent ) {
				hql = "select distinct item, title.simpleValue " + hqlPart + orderByClause;
			} else {
				hql = "select distinct item " + hqlPart + orderByClause;
			}
			list = executeFind(hql, queryInfo, args.toArray());
			if ( total == null ) {
				total = new Integer(list.size());
			}
			if ( localeIdentifierPresent ) {
				for ( ListIterator i = list.listIterator(); i.hasNext(); ) {
					Object[] objects = (Object[]) i.next();
					NewsItem newsItem = (NewsItem) objects[0];
					newsItem.getTitle().put(localeIdentifier, objects[1]);
					i.set(newsItem);
				}
			}
		} else {
			list = new ArrayList();
		}


		return new PartialCollection(list, total);

	}

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#getLastNews(com.blandware.atleap.common.util.QueryInfo)
	 */
	public List getLastNews(QueryInfo queryInfo) {

		String whereClause = "item.active = 'T'";
		String orderByClause = " order by item.publicationDate desc, title.simpleValue asc";

		List list = new ArrayList();

		String localeIdentifier = null;
		List roles = null;
		Integer limit = queryInfo.getLimit();
		if ( queryInfo != null ) {
			if ( queryInfo.getQueryParameters() != null ) {
				localeIdentifier = (String) queryInfo.getQueryParameters().get("localeIdentifier");
				roles = (List) queryInfo.getQueryParameters().get("roles");
			}
		}

		boolean localeIdentifierPresent = localeIdentifier != null && localeIdentifier.length() > 0;

		String hqlPart;
		ArrayList args = new ArrayList();
		if ( localeIdentifierPresent ) {
			args.add(localeIdentifier);
			hqlPart = new StringBuffer("from NewsItem as item ")
			        .append("left outer join item.contentFields as titleField ")
			        .append("left outer join item.contentFields as annotationField ")
			        .append("left outer join titleField.contentFieldValues as title ")
			        .append("left outer join annotationField.contentFieldValues as annotation ")
			        .append("where ")
			        .append("titleField.identifier = 'title' and annotationField.identifier = 'annotation' ")
			        .append("and title.contentLocale = annotation.contentLocale ")
			        .append("and title.contentLocale.identifier = ? ")
			        .append("and ").append(whereClause)
			        .toString();
		} else {
			hqlPart = "from NewsItem item where " + whereClause;
		}

		String hql;
		if ( localeIdentifierPresent ) {
			hql = "select item, title.simpleValue, annotation.value " + hqlPart + orderByClause;
		} else {
			hql = "select item " + hqlPart + orderByClause;
		}


		Integer offset = new Integer(0);
		// Iterate until requested number of elements will be acquired.
        // This tricky loop is needed because we cannot predict how much of news
        // items we'll need to fetch (as some of them don't match to the given
        // roles).
		do {
			List tmp = executeFind(hql, queryInfo, args.toArray(), true, "query.getLastNews");

			// if there are no more elements, break iteration
			if ( tmp.isEmpty() ) {
				break;
			}

			if ( localeIdentifierPresent ) {
				for ( ListIterator i = tmp.listIterator(); i.hasNext(); ) {
					Object[] objects = (Object[]) i.next();
					NewsItem newsItem = (NewsItem) objects[0];
					List itemRoles = newsItem.getRoles();
					boolean accessAllowed = true;
					if ( !itemRoles.isEmpty() ) {
						accessAllowed = false;
						if ( roles != null ) {
							for ( Iterator j = itemRoles.iterator(); j.hasNext() && !accessAllowed; ) {
								Role role = (Role) j.next();
								accessAllowed = roles.contains(role);
							}
						}
					}
					if ( !accessAllowed ) {
						i.remove();
					} else {
						byte[] annotation = (byte[]) objects[2];
						newsItem.getTitle().put(localeIdentifier, objects[1]);
						newsItem.getAnnotation().put(localeIdentifier, ConvertUtil.convertToString(annotation));
						i.set(newsItem);
					}
				}
			} else {
				// filter by roles
				for ( Iterator i = tmp.iterator(); i.hasNext(); ) {
					NewsItem item = (NewsItem) i.next();
					if ( !item.getRoles().equals(roles) ) {
						i.remove();
					}
				}
			}

			list.addAll(tmp);

			offset = new Integer(offset.intValue() + limit.intValue());
			queryInfo.setOffset(offset);
		} while ( list.size() < limit.intValue() );

		if ( list.size() > limit.intValue() ) {
			list = list.subList(0, limit.intValue());
		}

		return list;

	}


	// ~ Finders ================================================================

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#findNewsItemByUri(String)
	 */
	public NewsItem findNewsItemByUri(String newsItemUri) {
		return (NewsItem) findUniqueResult("from NewsItem newsItem where newsItem.uri = ?", new Object[]{newsItemUri});
	}

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#findUnpublishedNews(java.util.Date)
	 */
	public List findUnpublishedNews(Date date) {
		String hql = "from NewsItem item where item.active = 'F' and item.publicationDate <= ? and item.expirationDate > ?";
		return executeFind(hql, new Object[]{date, date}, new Type[]{Hibernate.DATE, Hibernate.DATE});
	}

	/**
	 * @see com.blandware.atleap.persistence.news.NewsDAO#findExpiredNews(java.util.Date)
	 */
	public List findExpiredNews(Date date) {
		String hql = "from NewsItem item where item.active = 'T' and item.expirationDate <= ?";
		return executeFind(hql, new Object[]{date}, new Type[]{Hibernate.DATE});
	}


	// ~ Helper methods

	/**
	 * Finds content newsItem by id in list
	 *
	 * @param newsItems list to search
	 * @param id        id of content newsItem
	 * @return menu item
	 */
	protected NewsItem findNewsItemById(List newsItems, Long id) {
		for ( int i = 0; i < newsItems.size(); i++ ) {
			NewsItem newsItem = (NewsItem) newsItems.get(i);
			if ( newsItem.getId().equals(id) ) {
				return newsItem;
			}
		}
		return null;
	}

	/**
	 * Collapses list by setting up content newsItems for different locales into one menu item maps
	 *
	 * @param queryResult query result with 5 objects (newsItem, locale, title, annotation, body)
	 * @return collapsed list
	 */
	protected List setLocalizableFields(List queryResult) {
		if ( queryResult == null || queryResult.size() <= 0 ) {
			return null;
		}

        // First make a list of news items from quintuples (assigning title, annotation
        // and body values with specified locale)
		List newsItems = new ArrayList(queryResult.size());
		Set processedLocales = new HashSet();
		for ( int i = 0; i < queryResult.size(); i++ ) {
			Object[] objects = (Object[]) queryResult.get(i);
			NewsItem newsItem = (NewsItem) objects[0];
			String locale = (String) objects[1];
			if ( locale != null && locale.trim().length() > 0 && !processedLocales.contains(locale) ) {
				processedLocales.add(locale);
				newsItem.getTitle().put(locale, objects[2]);
				newsItem.getAnnotation().put(locale, ConvertUtil.convertToString((byte[]) objects[3]));
				newsItem.getBody().put(locale, ConvertUtil.convertToString((byte[]) objects[4]));
			}

			newsItems.add(newsItem);
		}

        // Now make a such list of news items that each item is encountered maximum
        // one time (if some item is encountered second time, fore example, its
        // internal fields' info is added to previous instance)
		List fullNewsItems = new ArrayList(newsItems.size());
		for ( int i = 0; i < newsItems.size(); i++ ) {
			NewsItem newItem = (NewsItem) newsItems.get(i);
			NewsItem existingItem = findNewsItemById(fullNewsItems, newItem.getId());
			if ( existingItem != null ) {
				//title
				Iterator iterator = newItem.getTitle().keySet().iterator();
				while ( iterator.hasNext() ) {
					String localeIdentifier = (String) iterator.next();
					Object value = newItem.getTitle().get(localeIdentifier);
					if ( value == null ) {
						continue;
					}
					existingItem.getTitle().put(localeIdentifier, value);
				}

				//annotation
				iterator = newItem.getAnnotation().keySet().iterator();
				while ( iterator.hasNext() ) {
					String localeIdentifier = (String) iterator.next();
					Object value = newItem.getAnnotation().get(localeIdentifier);
					if ( value == null ) {
						continue;
					}
					existingItem.getAnnotation().put(localeIdentifier, value);
				}

				//body
				iterator = newItem.getBody().keySet().iterator();
				while ( iterator.hasNext() ) {
					String localeIdentifier = (String) iterator.next();
					Object value = newItem.getBody().get(localeIdentifier);
					if ( value == null ) {
						continue;
					}
					existingItem.getBody().put(localeIdentifier, value);
				}

			} else {
				fullNewsItems.add(newItem);
			}
		}

		return fullNewsItems;
	}

}
